Skip to content

🐛 fix(usage): clear in-flight lock on a successful fetch#487

Merged
sirmalloc merged 2 commits into
sirmalloc:mainfrom
gaborbernat:fix/clear-usage-lock-on-success
Jun 30, 2026
Merged

🐛 fix(usage): clear in-flight lock on a successful fetch#487
sirmalloc merged 2 commits into
sirmalloc:mainfrom
gaborbernat:fix/clear-usage-lock-on-success

Conversation

@gaborbernat

Copy link
Copy Markdown
Contributor

Switching Claude accounts could leave the usage widgets stuck on [Timeout] for up to 30 seconds even though the API was healthy and the new account would have fetched fine. 🔐 The trigger is the in-flight guard fetchUsageData writes before every API call: a usage.lock carrying { error: 'timeout', blockedUntil: now + LOCK_MAX_AGE }. On a successful fetch the cache is written and returned, but that pre-fetch lock is never removed, so it lingers for the full LOCK_MAX_AGE.

Inside that window any render that misses the file cache falls through to readActiveUsageLock and returns getStaleUsageOrError('timeout', …). An account switch is the clearest case: the fresh cache is rejected because the new token fails tokenHashMatches, the old account's cache is also rejected by fingerprint, and the leftover lock turns the result into { error: 'timeout' }. The label is misleading too, since usage.lock reads timeout after a fetch that actually succeeded.

The fix clears the lock once the successful response has been cached. ✨ Genuine error and rate-limit backoff locks are written on their own paths and stay untouched, so the existing "don't serve a mismatched account cache during an active lock" backoff behaviour is preserved.

Fixes #486

gaborbernat and others added 2 commits June 26, 2026 11:59
The pre-fetch usage.lock is written as a 'timeout' guard before the API
call but is never removed on success, so it lingers for LOCK_MAX_AGE. A
cache miss in that window (e.g. an account switch invalidating the token
fingerprint) then returns getStaleUsageOrError('timeout', ...), showing a
spurious [Timeout] while the API is healthy.

Clear the lock after a successful cache write. Genuine error and
rate-limit backoff locks are untouched.

Fixes sirmalloc#486
Only clear the in-flight usage lock after a successful API response satisfies the fields requested by the caller. Aggregate-only responses that are missing per-model fields remain cached, but keep the short timeout lock so later renders do not refetch the API on every status line render.

Add regression coverage for a weekly Sonnet usage request receiving an aggregate-only 200 response, verifying that the second fetch is throttled instead of issuing another API request.
@sirmalloc sirmalloc merged commit 08d3c7c into sirmalloc:main Jun 30, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Usage: pre-fetch lock not cleared on success → spurious [Timeout] after account switch

2 participants